Spring基础知识 AOP
什么是 AOP
参考资料 AOP 操作术语 参考资料 Spring AOP-api 参考资料 javaguide AOP
注:如果要直接使用可以看下面注解使用 AOP 那一节,它主要用于做日志输出,例如希望某个方法被调用时打印日志
AOP:Aspect Oriented Programming
面向切面的编程 实际上就是一种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想,它能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理
主要实现方式:
1、JDK 自带的代理:基于接口的动态代理技术(目标对象必须有接口)
2、cglib 代理:基于父类的动态代理技术(可以无需接口,因为是就是拿目标对象当父类,但是原理不是继承),CGLIB 包的底层是通过使用一个小而快的字节码处理框架 ASM(Java 字节码操控框架),来转换字节码并生成新的类。

AOP 的三个概念
关键就三个东西 “切入点”、“通知”(或者叫增强)、“切面”
通知/增强:增强的逻辑,称为增强,比如扩展日志功能,这个日志功能称为增强。如下:
@Before 前置通知:在方法之前执行
@AfterReturning 后置通知:在方法之后执行
@Around 环绕通知:在方法之前和之后执行
@AfterThrowing 异常通知:方法出现异常执行
@After 最终通知:无论是否有异常都会执行
切入点(PointCut):所谓切入点是指要对 哪些 连接点进行拦截(就是定义一个规则,这个规则就是切入点)
连接点(JointPoint):就是具体的某个需要被增强的方法
切面(Aspect):切面就是切入点这个规则匹配到的所有方法
具体例子说明
使用前需要在 beans 里加上 aop 的属性依赖
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
下面就是具体的切入通知的实例
<!-- 这两个就是要切进去的 “通知” -->
<bean id="beforeLog" class="com.alsritter.log.BeforeLog"/>
<bean id="afterLog" class="com.alsritter.log.AfterLog"/>
<!-- 要被切入的对象 -->
<bean id="userService" class="com.alsritter.service.UserServiceImp"/>
<!-- 配置 AOP 织入 -->
<aop:config>
<!-- 配置切入点规则:express:表达式,express(要执行的位置) 最后的这个.*(..) 表示所有方法-->
<!-- 可以看到,满足这个切入点规则的所有方法合称为一个 “切面” -->
<aop:pointcut id="pointcut" expression="execution(* com.alsritter.service.UserServiceImp.*(..))"/>
<!-- 注入通知 -->
<aop:after advice-ref="afterLog" pointcut-ref="pointcut"/>
<aop:before advice-ref="beforeLog" pointcut-ref="pointcut"/>
</aop:config>
两种切入方式
参考资料 aspect 与 advisor 的区别
<aop:aspect>:面向切面编程时,定义切面(切面包括通知和切点)
<aop:advisor>:进行事务管理时,定义通知器(通知器跟切面一样,也包括通知和切点)
上面两种都是用来切入通知的,只是它们的使用方式有点区别
在使用方式上 aspect 无需继承接口,直接定义一个类,指定其中哪个方法切入哪里就可以了,而 advisor 需要继承一个接口(例如 AfterReturningAdvice)才能完成切入
继承接口来编写通知的方式,每次只能编写一个特定的通知(例如继承了 AfterReturningAdvice 就只能写一个后置通知),所以如果不是必须使用到目标对象的方法体或其参数的话一般还是使用 <aop:aspect> 来进行切面编程
但是使用 advisor 也有其优点,继承接口的方式可以直接使用反射来取得被注入对象的方法
具体的使用例子看下面的 一个通知的切面、多个通知的切面
advisor 使用方法
可以直接定义一个通知类,使用这个通知类里面的方法对对象进行切入通知
定义一个通知类
定义一个类用作给切面提供通知
public class DiyPointCut {
public void before(){
System.out.println("========方法执行前=========");
}
public void after(){
System.out.println("========方法执行后=========");
}
}
AOP 的配置
....
<bean id="diy" class="com.alsritter.diy.DiyPointCut"/>
<aop:config>
<!-- 自定义切面,ref 要引用的类-->
<aop:aspect ref="diy">
<!-- 切入点-->
<aop:pointcut id="pointcut" expression="execution(* com.alsritter.service.UserServiceImp.*(..))"/>
<!-- 通知 -->
<aop:after method="after" pointcut-ref="pointcut"/>
<aop:before method="before" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
测试类
public class MyTest {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = (UserService) context.getBean("userService");
userService.add();
}
}
advisor 使用方法
这里使用的是 advisor,它需要让通知继承指定类型的接口,然后才能把这个通知切入到某个对象中
配置通知的接口
afterLog 继承 AfterReturningAdvice 接口
public class AfterLog implements AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("执行了"+method.getName()+"返回结果为:"+returnValue);
}
}
BeforeLog 继承 MethodBeforeAdvice 接口
public class BeforeLog implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println(target.getClass().getSimpleName()+"的"+method.getName()+"被执行了");
}
}
AOP 的配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 先配置好Bean -->
<bean id="beforeLog" class="com.alsritter.log.BeforeLog"/>
<bean id="afterLog" class="com.alsritter.log.AfterLog"/>
<bean id="userService" class="com.alsritter.service.UserServiceImp"/>
<!-- 配置 AOP 织入-->
<aop:config>
<!-- 配置切入点:express:表达式,express(要执行的位置) 最后的这个.*(..) 表示所有方法-->
<aop:pointcut id="pointcut" expression="execution(* com.alsritter.service.UserServiceImp.*(..))"/>
<!-- 注入通知 注意使用的是 advisor -->
<aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
<aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut"/>
</aop:config>
</beans>
这里的AOP切入点语法参考:【execution】
execution( [ 修饰符 ] 返回值类型 包名.类名.方法名( 参数 ))
测试类
调用时就像平常那样用就行了,可见,AOP 是完全不需要动源码
public class MyTest {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = (UserService) context.getBean("userService");
userService.add();
}
}
注解的方式定义
配置环境
只需导入这个 aspectjweaver,SpringAOP 已经集成到 Context 上了
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
然后在 beans 里加上 aop 的属性依赖
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
开启注解支持
<!-- 指定要扫描的包,这个包下的注解就会生效 -->
<context:component-scan base-package="com.alsritter.aop"/>
<!-- 配置 aop 自动代理 -->
<aop:aspectj-autoproxy/>
使用方法
@Before 前置通知:在方法之前执行
@AfterReturning 后置通知:在方法之后执行
@Around 环绕通知:在方法之前和之后执行
@AfterThrowing 异常通知:方法出现异常执行
@After 最终通知:无论是否有异常都会执行
在类里定义切面
@Aspect // 标注当前类是一个切面
@Component
public class AnnotationPointCut {
//传入一个切入点
@Before("execution(* com.alsritter.service.UserServiceImp.*(..))")
public void before(){
System.out.println("=========方法执行前========");
}
}
测试类同上
public class MyTest {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = (UserService) context.getBean("userService");
userService.add();
}
}
补充 JDK 动态代理
具体细节看 Java 代理模式那篇笔记,这里只是为了方便快速回忆